package generic.net.server;

import java.io.IOException;
import java.net.ServerSocket;
import generic.net.Message;
import generic.net.Peer;
import generic.net.observers.BroadcastDispatcher;
import generic.net.observers.PeerConnectDispatcher;
import generic.net.observers.PeerConnectObserver;
import generic.net.observers.PeerDisconnectDispatcher;
import generic.net.observers.PeerDisconnectObserver;

import java.net.Socket;

/**
 * Main socket listener, Waits for new incoming connections and creates
 * a new thread to handle communication with them.
 *
 * @author Adam Scarr <scarr.adam@gmail.com>
 */
public class Server implements Runnable {
    private ServerSocket socket;
    private boolean listening = false;
    private int port;
    private BroadcastDispatcher broadcastDispatcher = new BroadcastDispatcher();
    private ServiceManager serviceManager = new ServiceManager(this);
    private PeerDisconnectDispatcher peerDisconnectDispatcher = new PeerDisconnectDispatcher();
    private PeerConnectDispatcher peerConnectDispatcher = new PeerConnectDispatcher();

    /**
     * Create a new server that is not listening.
     *
     * You should call startServer manually.
     */
    public Server() {

    }

    /**
     * Constructor, creates a new server and binds it to the given port
     * @param port  The port to listen to incoming connections.
     */
    public Server(int port) {
        start(port);
    }

    /**
     * Starts listening for incoming connections and forking new client threads.
     */
    public void start(int port) {
        this.port = port;
        listening = true;

        try {
            socket = new ServerSocket(port);
            System.out.println("[SERVER] Bound to port " + port);
        } catch (IOException ex) {
            System.out.println("[SERVER] Unable to bind to port " + port);
            System.exit(0);
        }

        new Thread(this, "Serversocket listener").start();
    }

    /**
     * Stops listening and closes all existing connections.
     */
    public void stop() {
        sendAll(new Message("CLOSED"));

        while (broadcastDispatcher.size() > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException ex) {}
        }

        try {
            socket.close();
            while (!socket.isClosed()) {}
        } catch (IOException ex) {
            System.out.println("[SERVER] Could not close connection, its probably been closed for us...");
        }

        listening = false;
    }

    /**
     * Listens for new connections, and creating new clients to handle them.
     */
    public void run() {
        while (listening) {
            Socket socket;
            try {
                socket = this.socket.accept();
            } catch (IOException ex) {
                System.out.println("[SERVER] Server connection closed, no longer listening for new clients.");
                listening = false;
                break;
            }

            System.out.println("[SERVER] Client connected to server...");
            // A client has connected so lets start a thread to get messages.
            Peer peer = createPeer();

            peer.getPeerConnectDispatcher().add(new PeerConnectObserver() {
                public void peerConnect(Peer peer) {
                    peerConnected(peer);
                }
            });

            peer.connect(socket);
        }
    }

    /**
     * Returns the current ServiceManager.
     * @return the current ServiceManager.
     */
    public ServiceManager getServiceManager() {
        return serviceManager;
    }

    /**
     * Factory method for creating a new peer.
     * @return A new peer.
     */
    protected Peer createPeer() {
        return new Peer();
    }

    /**
     * Sends a message to everyone on the broadcast list.
     * @param msg The message to send.
     */
    public void sendAll(Message msg) {
        broadcastDispatcher.notifyBroadcast(msg);
    }

    private void peerConnected(Peer peer) {
        // Then setup the broadcast and disconnect events.
        broadcastDispatcher.add(peer);
        peer.getPeerDisconnectDispatcher().add(new PeerDisconnectObserver() {
            public void peerDisconnect(Peer peer) {
                peerDisconnected(peer);
            }
        });

        // Finally bind the servicemanager to the client, so that it
        // can respond to new service events.
        peer.getMessageDispatcher().add(serviceManager);
        peer.getRequestDispatcher().add(serviceManager);

        peerConnectDispatcher.notifyPeerConnect(peer);
    }

    /**
     * When a peer notifys us that the connection was lost we should remove them
     * from further broadcasts.
     * @param peer  The peer that has disconnected.
     */
    private void peerDisconnected(Peer peer) {
        broadcastDispatcher.remove(peer);
        System.out.println("[SERVER] Connection to client closed, " + broadcastDispatcher.size() + " left");

        peerDisconnectDispatcher.notifyPeerDisconnect(peer);
    }

    public PeerConnectDispatcher getPeerConnectDispatcher() {
        return peerConnectDispatcher;
    }

    public PeerDisconnectDispatcher getPeerDisconnectDispatcher() {
        return peerDisconnectDispatcher;
    }

    /**
     * Returns the current broadcast dispatcher.
     * @return  The current broadcast dispatcher.
     */
    public BroadcastDispatcher getBroadcastDispatcher() {
        return broadcastDispatcher;
    }

}
